#!/usr/bin/env python2.4

# strategy:
# - on startup:
#   - make a new dead window
#   - if there's a filename given, try to load the sgf file (exceptions, dialogs, ...)
#   - if success, set_sgf(); else, do nothing.
# - on new:
#   - make a new dead window
# - on open:
#   - try to load the sgf file (exceptions, dialogs, ...)
#   - if success,  [= make new window with loaded sgf-tree]
#     - if not dead-window: make new dead-window
#     - set its sgf-tree to the loaded sgf
#   - else, do nothing

# ops:
# - make a new dead window
# - try to load sgf file (filename->game); show dialogs on errors
# - set_sgf()  (only needs to be defined for dead windows)
# sounds good.

# note: a "dead window" is one that's not displaying anything.  the
# opposite is a window that's displaying an SGF file.  this is an
# important distinction to make, because when the user chooses
# file->open on a dead window, i'll open the file in that window,
# instead of making a new one (which is probably not what he wanted).
# that is, if you start gimme from a menu, you see an empty window; if
# you then choose file->open, you probably don't want that file *and*
# an empty window open.

# ----------------------------------------

# BUG:
# - when you close a game window, the get-info window doesn't close, too (make it a dialog?)

# WEIRD:
# - may be bugs with closing windows, or it may be just the canvas warnings
# - resizing game-info window
# - why do i have file->new, anyway?
# - should a dead-window be blank, or have an empty board?
# - new (dead) windows have a lot of ??'s in the initial display.

import gtk
import gtk.glade
import gnome
import gconf

import os

if not os.getenv("NO_PSYCO"):
    try:
        import psyco
        psyco.full()
    except ImportError:
        pass

import sgf
import sgfpreview

from cairoboard import *
from gametreeview import *

from gowiki import *

# TODO: hide save-as and view*, until they're implemented.  (edit* can stay.)

# NOTE: if you use tihs, be sure to:
# # dim "get game info" if there's no game.
# # self.xml.get_widget("get_game_info1").set_sensitive(False)
# and:
# self.sgf = ...
def try_to_load(filename, window):
    """try to load the sgf-file in filename;
    if it can't be loaded, show the appropriate dialogs, etc.
    returns the sgf-tree, or None on failure.
    window is the parent-window for error dialogs."""
    import pyparsing

    # parse the sgf file -- FIXME: don't barf if there's no arg (filename).
    try:
        return sgf.load(filename)
    except IOError, ioe:
        errno,msg = ioe

        # show a dialog telling the user what's wrong.
        rel_filename = filename.split(os.path.sep)[-1] # extract filename only
        text = """<big><b>Could not open the file "%s".</b></big>

The problem was:
Error %d: %s.""" % (rel_filename, errno, msg)
        dialog = gtk.MessageDialog(parent=window,
                                   flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                                   type=gtk.MESSAGE_WARNING,
                                   buttons=gtk.BUTTONS_OK,
                                   message_format=None)
        dialog.set_markup(text) # (need a separate call to get html markup, apparently)
        dialog.set_icon_from_file("Resources/2-stones.png")

        dialog.run()
        dialog.destroy()

        return None
    except pyparsing.ParseException, ex:
        # show a dialog telling the user what's wrong.
        rel_filename = filename.split(os.path.sep)[-1] # extract filename only
        text = """<big><b>Could not open the file "%s".</b></big>

The parse error was:
    %s
Either this isn't an SGF file, or it has been corrupted.""" % (rel_filename, ex)
        dialog = gtk.MessageDialog(parent=window,
                                   flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                                   type=gtk.MESSAGE_WARNING,
                                   buttons=gtk.BUTTONS_OK,
                                   message_format=None)
        dialog.set_markup(text) # (need a separate call to get html markup, apparently)
        dialog.set_icon_from_file("Resources/2-stones.png")
        dialog.run()
        dialog.destroy()

        return None

    # TODO: add "except Exception,e" clause, and say "found bug"?

# members: xml [RENAME to glade?], window?, size, board, sgf
class PlaybackWindow: # (not a subclass of gtk.Window: i'l use composition, so i can use libglade)
    def __init__(self, filename=None):
        self.filename = filename

        self.xml = gtk.glade.XML("Resources/gimme.glade")

        self.about = None # HACK!

        # disable menuitems that i haven't implemented yet
        dead_commands = [# 'save_as1',
                         'undo1', 'cut1', 'copy1', 'paste1', 'clear1',
                         #'flip_board1'
                         ]
        for c in dead_commands:
            self.xml.get_widget(c).set_sensitive(False)

        # gtk event handlers (these are the same for normal windows and dead windows)
        self.xml.signal_autoconnect({"on_get_info_activate": self.on_get_info_activate,
                                     "on_window1_delete_event": self.on_window1_delete_event,
                                     "on_menu_open": self.on_menu_open,
                                     "on_hide_info": self.on_hide_info,
                                     "on_menu_new": self.on_menu_new,
                                     "on_menu_save_as": self.on_menu_save_as,
                                     "on_open_go_wiki_activate": self.on_open_go_wiki_activate,
                                     "on_gimme_help_activate": self.on_gimme_help_activate,
                                     "on_about_activate": self.on_about_activate,
                                     "on_menu_quit": self.on_menu_quit,
                                     "on_toggle_coordinates_activate": self.on_toggle_coords})

        # gconf event handlers
        client = gconf.client_get_default()
        client.add_dir('/apps/gimme/display', gconf.CLIENT_PRELOAD_NONE)
        self.gconf_handlers = [
            client.notify_add('/apps/gimme/display/coordinates', self.on_gconf_coords_changed),
            ]
        self.on_gconf_coords_changed() # (initial setting)

        # WRITEME: if filename==None, do stuff here (make this a dummy window)
        if filename == None:
            self.sgf = None
            # (no gametree -- there would be nothing in it, anyway)

            # WRITEME (but not here): if you file->open and self.sgf==None, do all the setup stuff
            # -- does this mean i should move all this to a self.load(filename) method?
            # (that would probably make life easier...)

            # dim "get game info" if there's no game.
            self.xml.get_widget("get_game_info1").set_sensitive(False)

            return

        # try to load file as sgf-tree
        self.sgf = try_to_load(self.filename, self.xml.get_widget("window1"))

        # dim "get game info" if there's no game.
        if self.sgf is None:
            self.xml.get_widget("get_game_info1").set_sensitive(False)

        # do setup stuff
        if self.sgf:
            self.set_sgf(self.sgf)

    def on_cursor_moved(self, game, cursor):
        # update comments
        if 'C' in cursor.node.properties:
            self.xml.get_widget("game_comments").get_buffer().set_text(cursor.node.properties['C'])
        else:
            self.xml.get_widget("game_comments").get_buffer().set_text("")

        # update capture scores
        self.xml.get_widget('black_captures').set_text(str(cursor.board.black_prisoners))
        self.xml.get_widget('white_captures').set_text(str(cursor.board.white_prisoners))

        # update territory scores
        if 'TW' in cursor.node.properties:
            white_territory = len(cursor.node.properties['TW'])
            self.xml.get_widget('white_territory').set_text(str(white_territory))
        else:
            self.xml.get_widget('white_territory').set_text(u'\N{EM DASH}')
        if 'TB' in cursor.node.properties:
            black_territory = len(cursor.node.properties['TB'])
            self.xml.get_widget('black_territory').set_text(str(black_territory))
        else:
            self.xml.get_widget('black_territory').set_text(u'\N{EM DASH}')

        from count import count # MOVE THIS UP TOP!

        # update dead scores (= number of TW/TB points with opponent's stones here)
        white_dead = 0
        if 'TB' in cursor.node.properties:
            white_dead = count(pt for pt in cursor.node.properties['TB'] if cursor.board.get(pt)==WHITE)
        if white_dead:
            self.xml.get_widget('white_captured').set_text("(%d)" % white_dead)
        else:
            self.xml.get_widget('white_captured').set_text(u'\N{EM DASH}')

        black_dead = 0
        if 'TW' in cursor.node.properties:
            black_dead = count(pt for pt in cursor.node.properties['TW'] if cursor.board.get(pt)==BLACK)
        if black_dead:
            self.xml.get_widget('black_captured').set_text("(%d)" % black_dead)
        else:
            self.xml.get_widget('black_captured').set_text(u'\N{EM DASH}')

        # update total scores = captures + [komi] + territory - dead.
        # only show a total if this player's territory is known.
        try:
            black_captures = int(self.xml.get_widget('black_captures').get_text())
            black_territory = int(self.xml.get_widget('black_territory').get_text())
            total = black_captures + black_territory - black_dead
            self.xml.get_widget('black_total').set_text(str(total))
        except:
            self.xml.get_widget('black_total').set_text(u'\N{EM DASH}')
        try:
            komi = float(game.head.properties['KM'])
        except (KeyError,ValueError):
            komi = 0.0
        try:
            white_captures = int(self.xml.get_widget('white_captures').get_text())
            white_territory = int(self.xml.get_widget('white_territory').get_text())
            total = white_captures + komi + white_territory - white_dead
            self.xml.get_widget('white_total').set_text(str(total))
        except:
            self.xml.get_widget('white_total').set_text(u'\N{EM DASH}')

    def set_sgf(self, sgf):
        """sgf is a Game."""
        self.sgf = sgf

        # go-game event handlers
        self.sgf.connect("cursor_moved", self.on_cursor_moved)

        # replace treeview with my own
        oldtreeview = self.xml.get_widget("treeview1")
        parent = oldtreeview.get_parent()
        parent.remove(oldtreeview)
        oldtreeview.destroy()
        newtreeview = GameView(self.sgf)
        parent.add(newtreeview)
        newtreeview.show()

        # adjust the min/max positions of the sliders
        # DOESN'T WORK: get error "property min-position is not writable" -- but the docs say it is!
#       self.xml.get_widget("hpaned1").set_property("min-position", 100)
#       self.xml.get_widget("hpaned1").set_property("max-position", 1000) # FIXME: should be like -100
#       self.xml.get_widget("vpaned1").set_property("min-position", 100)
#       self.xml.get_widget("vpaned1").set_property("max-position", 1000) # FIXME: should be like -100

        # replace canvas with my own
        oldcanvas = self.xml.get_widget("canvas1")
        parent = oldcanvas.get_parent()
        parent.remove(oldcanvas)
        oldcanvas.destroy()
        newcanvas = CairoBoard(self.sgf)
        parent.add(newcanvas)
        newcanvas.show()

        # set komi display
        if 'KM' in self.sgf.head.properties:
            self.xml.get_widget('white_komi').set_text(self.sgf.head.properties["KM"])
        else:
            self.xml.get_widget('white_komi').set_text(u'\u2014')

        # focus it
        newtreeview.grab_focus()

        # title should be "$(black) ($(rank)) v $(white) ($(rank))"; if no filename, simply "Gimme".
        try:
            # CHECK: will one player's name ever be missing, and should i do more in that case?
            black = self.sgf.head.properties["PB"]
            if 'BR' in self.sgf.head.properties:
                black += " (%s)" % self.sgf.head.properties["BR"]
            white = self.sgf.head.properties["PW"]
            if 'WR' in self.sgf.head.properties:
                white += " (%s)" % self.sgf.head.properties["WR"]
            self.xml.get_widget("window1").set_title("%s v %s" % (black, white))
        except:
            pass

    # ----------------------------------------
    # signal handlers:

    def on_get_info_activate(self, menuitem):
        if 'GN' in self.sgf.head.properties:
            self.xml.get_widget("title").set_text(self.sgf.head.properties["GN"].strip())

        if 'PB' in  self.sgf.head.properties:
            self.xml.get_widget("black_name").set_text(self.sgf.head.properties["PB"])
        if 'BR' in self.sgf.head.properties:
            self.xml.get_widget("black_rank").set_text(self.sgf.head.properties["BR"])
        if 'HA' in self.sgf.head.properties:
            self.xml.get_widget("handicap").set_text(self.sgf.head.properties["HA"])

        if 'PW' in self.sgf.head.properties:
            self.xml.get_widget("white_name").set_text(self.sgf.head.properties["PW"])
        if 'WR' in self.sgf.head.properties:
            self.xml.get_widget("white_rank").set_text(self.sgf.head.properties["WR"])
        if 'KM' in self.sgf.head.properties:
            self.xml.get_widget("komi").set_text(self.sgf.head.properties["KM"])

        if 'TM' in self.sgf.head.properties:
            self.xml.get_widget("time").set_text(self.sgf.head.properties["TM"]) # FIXME: parse this
        if 'RE' in self.sgf.head.properties:
            self.xml.get_widget("result").set_text(self.sgf.head.properties["RE"])

        if 'DT' in self.sgf.head.properties:
            self.xml.get_widget("date").set_text(self.sgf.head.properties["DT"]) # FIXME: localize?
        if 'PC' in self.sgf.head.properties:
            self.xml.get_widget("place").set_text(self.sgf.head.properties["PC"])
        if 'EV' in self.sgf.head.properties:
            self.xml.get_widget("event").set_text(self.sgf.head.properties["EV"])

        if 'SO' in self.sgf.head.properties:
            self.xml.get_widget("source").set_text(self.sgf.head.properties["SO"])
        if 'CP' in self.sgf.head.properties:
            self.xml.get_widget("copyright").set_text(self.sgf.head.properties["CP"])

        if 'C' in self.sgf.head.properties:
            self.xml.get_widget("comments").get_buffer().set_text(self.sgf.head.properties["C"])

        # the info window
        window = self.xml.get_widget('info_window')

        # make this a child window
        window.set_transient_for(self.xml.get_widget('window1'))

        # show it
        window.show_all()

    # (when main window is closed)
    def on_window1_delete_event(self, window, event):
        # remove gconf listeners, since my window will no longer exist
        client = gconf.client_get_default()
        for handler_id in self.gconf_handlers:
            client.notify_remove(handler_id)

        window.destroy()
        gtk.main_quit()
        return True

    def on_menu_new(self, menuitem):
        PlaybackWindow()
        gtk.main()

    def on_menu_save_as(self, menuitem):
        dialog = sgfpreview.make_dialog(title="Save as",
                                        action=gtk.FILE_CHOOSER_ACTION_SAVE)
        if dialog.run() == gtk.RESPONSE_OK:
            filename = dialog.get_filename()
            dialog.destroy()
            sgf.save(self.sgf, filename) # TODO: catch exceptions here!
        else:
            dialog.destroy()

        return True

    def on_menu_open(self, menuitem):
        dialog = sgfpreview.make_dialog(title="Choose an SGF file",
                                        action=gtk.FILE_CHOOSER_ACTION_OPEN)
        response = dialog.run()
        if response == gtk.RESPONSE_OK:
            filename = dialog.get_filename()
            dialog.destroy()

            # try to load
            new_sgf = try_to_load(filename, self.xml.get_widget("window1"))

            # (failed!)
            if new_sgf is None:
                return True

            if self.sgf:
                # i'm not a dead window: need to make a new one
                window = PlaybackWindow()
                window.set_sgf(new_sgf)
                window.xml.get_widget("get_game_info1").set_sensitive(True)
                gtk.main()
            else:
                # new sgf
                self.xml.get_widget("get_game_info1").set_sensitive(True)
                self.set_sgf(new_sgf)
        else:
            dialog.destroy()

        return True

    def on_hide_info(self, window, event):
        self.xml.get_widget('info_window').hide()
        return True # =stop propagating event

    def on_gconf_coords_changed(self, *args):
        if gconf.client_get_default().get_bool("/apps/gimme/display/coordinates"):
            text = "Hide _Coordinates"
        else:
            text = "Show _Coordinates"
        self.xml.get_widget("toggle_coordinates").get_child().set_markup_with_mnemonic(text)

    def on_toggle_coords(self, menuitem):
        client = gconf.client_get_default()
        old_value = client.get_bool("/apps/gimme/display/coordinates")
        client.set_bool("/apps/gimme/display/coordinates", not old_value)
        # FUTURE: add gconf schema stuff, so if you poke around in there you see pretty documentation

    def on_open_go_wiki_activate(self, menuitem): # RENAME?: on_menu_help_gowiki()?
        open_go_wiki()
        return True

    def on_gimme_help_activate(self, menuitem):
        # FIXME: install it in a standard place, so this can be simply "ghelp:Gimme"
        gnome.help_display_uri("ghelp:/home/kharris/Temporary/Gimme/Manual.xml")
        # TODO: catch errors here

    def on_about_activate(self, menuitem):
        if self.about:
            self.about.show_all()
            return

        # make a new XML object (don't worry, they're cached).
        # it doesn't like re-using libglade dialogs, i guess.
        xml = gtk.glade.XML("Resources/gimme.glade", "about3")

        xml.signal_autoconnect({"on_about_hide": self.on_about_hide})

        self.about = xml.get_widget('about3')
        self.about.set_property("name", "Gimme") # why isn't this set in glade, or by gnome_init?  beats me.
        self.about.set_property("version", "0.1") # (ditto)
        self.about.show_all()
        return True

    def on_about_hide(self, dialog, id):
        self.about = None
        return False # =keep propagating event

    def on_menu_quit(self, menuitem):
        window = self.xml.get_widget("window1")
        window.destroy()
        gtk.main_quit()
        return True

    # TODO:
    # - when replay-window closes, close info-window, too.
    # - add gocanvas.destroy(), which kills all canvas items -- call .destroy() when window closes.

    # - make get-info-window entries non-editable (in glade)
    # - (make get-info-window a dialog?)
    # - make get-info-window friendlier, e.g., "B+4" -> "Black, by 4 points"
    # - in get-info-window, esc and/or ctrl-W should close window

    # - is it possible to set a minimum size on the *paned's?

    # - if multiple games, let the user choose (Game in menubar?  popup between toolbar and moves?)
    # --- needs sgf parser work, first

    # - in menuitems, only show my icons if the user has icons enabled (pygtk issue?) (nah, doubtful.)
    # --- or simply never show icons?

    # - fix filenames (.glade, .png), so it'll run ok from anywhere -- (how?)

    # - (if you hold down shift over the board, show [lightly] all variants from this point)

    # - (add progressbar, if parsing really long sgf)

    # (files to ship: sgf, gametreeview, gocanvas, gimme, board, point, handicap ... ?)

# ----------------------------------------
def main():
    import signal
    signal.signal(signal.SIGINT, signal.SIG_DFL) # make ctrl-C quit, normally
    gnome.init("Gimme", "0.1")
    import sys
    if len(sys.argv) >= 2:
        filename = sys.argv[1]
        PlaybackWindow(filename)
        gtk.main() # these can be nested, so i call it once per window -- when all are closed, quit.
    else:
        PlaybackWindow()
        gtk.main()

    # whenever you make a new PlaybackWindow, you should then call gtk.main().
    # closing a PlaybackWindow calls gtk.main_quit().

if __name__ == '__main__':
    main()
